#!/bin/sh
#
# Copyright (c) 2011-2016 Dmitry V. Levin <ldv@altlinux.org>
# Copyright (c) 2011-2020 The strace developers.
# Copyright (C) 2020 Gleb Fotengauer-Malinovskiy <glebfm@altlinux.org>
# Copyright (c) 2021 Ivan Zakharyaschev <imz@altlinux.org>
# All rights reserved.
#
# SPDX-License-Identifier: GPL-2.0-or-later

export LC_ALL=C
ME_="${0##*/}"
LOG="log"
OUT="out"
EXP="exp"

warn_() { printf >&2 '%s\n' "$*"; }
fail_() { warn_ "$ME_: failed test: $*"; exit 1; }
skip_() { warn_ "$ME_: skipped test: $*"; exit 77; }
framework_failure_() { warn_ "$ME_: framework failure: $*"; exit 99; }
framework_skip_() { warn_ "$ME_: framework skip: $*"; exit 77; }

# get_config_str OPTION
#
# Returns the value of OPTION from config.h (path to which set
# in the CONFIG_H variable).
get_config_str()
{
	sed -r -n 's/#define[[:space:]]*'"$1"'[[:space:]]*"([^"]*)".*/\1/p' \
		"$CONFIG_H"
}

# get_config_option OPTION YES_STRING [NO_STRING]
#
# Returns YES_STRING in case OPTION is enabled (present in config.h and has
# a non-zero numeric value). Otherwise, NO_STRING (or empty string, if not
# specified) is returned.
get_config_option()
{
	local opt
	opt=$(sed -r -n 's/#define[[:space:]]*'"$1"'[[:space:]]*([0-9]+)$/\1/p' \
		"$CONFIG_H")
	if [ -n "$opt" -a "$opt" -ne 0 ]; then
		printf "%s" "$2"
	else
		printf "%s" "${3-}"
	fi
}

check_prog()
{
	type "$@" > /dev/null 2>&1 ||
		framework_skip_ "$* is not available"
}

dump_log_and_fail_with()
{
	cat < "$LOG" >&2
	fail_ "$*"
}

run_prog()
{
	if [ $# -eq 0 ]; then
		set -- "../$NAME"
	fi
	args="$*"
	"$@" || {
		rc=$?
		if [ $rc -eq 77 ]; then
			skip_ "$args exited with code 77"
		else
			fail_ "$args failed with code $rc"
		fi
	}
}


run_prog_skip_if_failed()
{
	args="$*"
	"$@" || framework_skip_ "$args failed with code $?"
}

try_run_prog()
{
	local rc

	"$@" > /dev/null || {
		rc=$?
		if [ $rc -eq 77 ]; then
			return 1
		else
			fail_ "$* failed with code $rc"
		fi
	}
}

check_gawk()
{
	check_prog gawk
	check_prog grep

	local program="$1"; shift
	if grep '^@include[[:space:]]' < "$program" > /dev/null; then
		gawk '@include "/dev/null"' < /dev/null ||
			framework_skip_ 'gawk does not support @include'
	fi
}

# Usage: [FILE_TO_CHECK [AWK_PROGRAM [ERROR_MESSAGE [EXTRA_AWK_OPTIONS...]]]]
# Check whether AWK_PROGRAM matches FILE_TO_CHECK using gawk.
# If it doesn't, dump FILE_TO_CHECK and fail with ERROR_MESSAGE.
match_awk()
{
	local output program error
	if [ $# -eq 0 ]; then
		output="$LOG"
	else
		output="$1"; shift
	fi
	if [ $# -eq 0 ]; then
		program="$srcdir/$NAME.awk"
	else
		program="$1"; shift
	fi
	if [ $# -eq 0 ]; then
		error="$ME_ $args output mismatch"
	else
		error="$1"; shift
	fi

	check_gawk "$program"

	AWKPATH="$srcdir" gawk -f "$program" "$@" < "$output" || {
		cat < "$output"
		fail_ "$error"
	}
}

# Usage: [FILE_TO_CHECK [FILE_TO_COMPARE_WITH [ERROR_MESSAGE]]]
# Check whether FILE_TO_CHECK differs from FILE_TO_COMPARE_WITH.
# If it does, dump the difference and fail with ERROR_MESSAGE.
match_diff()
{
	local output expected error
	if [ $# -eq 0 ]; then
		output="$LOG"
	else
		output="$1"; shift
	fi
	if [ $# -eq 0 ]; then
		expected="$srcdir/$NAME.expected"
	else
		expected="$1"; shift
	fi
	if [ $# -eq 0 ]; then
		error="$ME_: output mismatch"
	else
		error="$1"; shift
	fi

	check_prog diff

	diff -u -- "$expected" "$output" ||
		fail_ "$error"
}

# Usage: [FILE_TO_CHECK [FILE_WITH_PATTERNS [ERROR_MESSAGE]]]
# Check whether all patterns listed in FILE_WITH_PATTERNS
# match FILE_TO_CHECK using egrep.
# If at least one of these patterns does not match,
# dump both files and fail with ERROR_MESSAGE.
match_grep()
{
	local output patterns error pattern cnt failed= rc negated
	if [ $# -eq 0 ]; then
		output="$LOG"
	else
		output="$1"; shift
	fi
	if [ $# -eq 0 ]; then
		patterns="$srcdir/$NAME.expected"
	else
		patterns="$1"; shift
	fi
	if [ $# -eq 0 ]; then
		error="$ME_ $args output mismatch"
	else
		error="$1"; shift
	fi

	check_prog wc
	check_prog grep

	cnt=1
	while read -r pattern; do
		negated=0
		[ "x${pattern#!}" = "x${pattern}" ] ||
			negated=1

		rc="$negated"
		LC_ALL=C grep -E -x -e "${pattern#!}" < "$output" > /dev/null ||
			rc="$((!negated))"

		[ 0 = "$rc" ] || {
			test -n "$failed" || {
				echo 'Failed patterns of expected output:'
				failed=1
			}
			printf '#%d: %s\n' "$cnt" "$pattern"
		}
		cnt=$(($cnt + 1))
	done < "$patterns" ||
		fail_ "Error reading patterns from \"$patterns\""
	test -z "$failed" || {
		echo 'Actual output:'
		cat < "$output"
		fail_ "$error"
	}
}

_Architectures=
_Archive=
_Codename=
_Components=
_Date=
_Description=
_Label=
_Origin=
_Suite=
_Version=

parse_release_field() {
	local F="$1"; shift
	local field="$1"; shift
	sed -n "s/^$field: //p" "$F"
}

parse_release() {
	local F="$1"; shift

	_Architectures="$(parse_release_field "$F" Architectures)"
	_Archive="$(parse_release_field "$F" Archive)"
	_Codename="$(parse_release_field "$F" Codename)"
	_Components="$(parse_release_field "$F" Components)"
	_Date="$(parse_release_field "$F" Date)"
	_Description="$(parse_release_field "$F" Description)"
	_Label="$(parse_release_field "$F" Label)"
	_Origin="$(parse_release_field "$F" Origin)"
	_Suite="$(parse_release_field "$F" Suite)"
	_Version="$(parse_release_field "$F" Version)"
}

find_repo() {
	local name
	name="$1"; shift
	realpath "$srcdir/$name".repo
}

copy_pkgs() {
	local arch="$1"; shift
	local dir="$repo/$arch"
	[ ! -d "$dir" ] || rm -r "$dir"
	mkdir -p "$dir"
	cp -Rs $(set +f; echo "$repo_expected/$arch"/RPMS.*) -t "$dir"
	srpm_dirs="$(set +f && echo "$repo_expected/$arch"/SRPMS.*)"
	for d in $srpm_dirs; do
		[ -d "$d" ] || continue
		cp -Rs "$d" -t "$dir"
	done
	srpm_dirs="$(set +f && echo "$repo_expected"/SRPMS.*)"
	for d in $srpm_dirs; do
		[ -d "$d" ] || continue
		cp -Rs "$d" -t "$repo"
	done
}

copy_base() {
	local -r src="$1"/base; shift
	local -r dst="$1"/base; shift

	cp -R "$src" -T "$dst"
}

basedir=base
compare_filelist() {
	check_prog find
	check_prog diff

	local arch="$1"; shift
	find "$repo_expected/$arch/$basedir" -type f -printf '%P\n' > "$NAME.$arch".filelist.expected
	find "$repo/$arch/$basedir" -type f -printf '%P\n' > "$NAME.$arch".filelist
	sort -u -o "$NAME.$arch".filelist.expected{,}
	sort -u -o "$NAME.$arch".filelist{,}
	diff -u "$NAME.$arch".filelist.expected "$NAME.$arch".filelist
}

skip_compare_release=
compare_release() {
	check_prog sed
	check_prog diff

	local arch="$1"; shift
	local some_date='Thu, 01 Jan 1970 00:00:00 +0000'
	sed \
		"s/^Date: .*/Date: $some_date/" \
		"$repo_expected/$arch/$basedir"/release \
		> "$NAME.$arch".release.expected
	sed \
		"s/^Date: .*/Date: $some_date/" \
		"$repo/$arch/$basedir"/release \
		> "$NAME.$arch".release
	[ "$skip_compare_release" = yes ] ||
		diff -u "$NAME.$arch".release.expected "$NAME.$arch".release
	printf '%s\n' release >> "$NAME.$arch".checked_files
}

compare_comp_releases() {
	check_prog cmp

	local arch="$1"; shift
	for rel in $(set +f; echo "$repo/$arch/$basedir"/release.*); do
		bname=${rel##*/}
		diff -u "$repo_expected/$arch/$basedir/$bname" "$rel"
		printf "%s\n" "$bname" >> "$NAME.$arch".checked_files
	done
}

uncompress_pkglist() {
	local from="$1"; shift
	local to="$1"; shift
	case "$from" in
		*.zst)
			zstd -d < "$from" > "$to"
			;;
		*.xz)
			xz -d < "$from" > "$to"
			;;
		*.bzip2)
			bzip2 -d < "$from" > "$to"
			;;
		*)
			cat "$from" > "$to"
			;;
	esac
}

strip_size_md5=
compare_list() {
	local bname="$1"; shift
	local exp="$1"; shift
	local res="$1"; shift
	check_prog cmp
	if [ "$strip_size_md5" = yes ]; then
		uncompress_pkglist "$exp" "$bname".exp.uncompressed
		exp="$bname".exp.uncompressed
		uncompress_pkglist "$res" "$bname".uncompressed
		res="$bname".uncompressed

		$STRIP_SIZE_MD5 "$exp" "$bname".exp.stripped
		exp="$bname".exp.stripped
		$STRIP_SIZE_MD5 "$res" "$bname".stripped
		res="$bname".stripped
	fi

	cmp "$exp" "$res"
}

compare_pkglists() {
	local arch="$1"; shift
	for list in $(set +f; echo "$repo/$arch/$basedir"/pkglist.*); do
		bname=${list##*/}
		compare_list "$bname" "$repo_expected/$arch/$basedir/$bname" "$list"
		printf "%s\n" "$bname" >> "$NAME.$arch".checked_files
	done
}

compare_srclists() {
	local arch="$1"; shift
	for list in $(shopt -s nullglob; set +f; echo "$repo/$arch/$basedir"/srclist.*); do
		bname=${list##*/}
		compare_list "$bname" "$repo_expected/$arch/$basedir/$bname" "$list"
		printf "%s\n" "$bname" >> "$NAME.$arch".checked_files
	done
}

unchecked_files() {
	check_prog sort

	local arch="$1"; shift
	sort -u -o "$NAME.$arch".checked_files{,}
	comm -13 "$NAME.$arch".checked_files "$NAME.$arch".filelist > "$NAME.$arch".unchecked_files
	if [ -s "$NAME.$arch".unchecked_files ]; then
		cat "$NAME.$arch".unchecked_files
		return 1
	fi
	return 0
}

check_base() {
	local arch="$1"; shift
	compare_filelist "$arch"
	compare_release "$arch"
	compare_comp_releases "$arch"
	compare_pkglists "$arch"
	compare_srclists "$arch"
	unchecked_files "$arch"
}

case "$ME_" in
	*.gen.test) NAME="${ME_%.gen.test}" ;;
	*.test) NAME="${ME_%.test}" ;;
	*) NAME=
esac

tmpdir="$PWD/$NAME".tmp
mkdir -p "$tmpdir"

repo="$PWD/$NAME".repo
repo_expected="$srcdir/$NAME.expected"
repo_expected="$(realpath "$repo_expected")"

SRC="$srcdir"/../src
builddir="$(realpath ..)"
PATH="$builddir/src:$PATH"
[ -n "${GENBASEDIR-}" ] || GENBASEDIR="$SRC/genbasedir"
[ -n "${GENPKGLIST-}" ] || GENPKGLIST="$builddir/src/genpkglist"

[ -n "${STRIP_SIZE_MD5-}" ] || STRIP_SIZE_MD5="$builddir"/src/strip-size-and-md5-tags

[ -z "${VERBOSE-}" ] ||
	set -x
